<template>
  <div
    :style="`
      background: ${props.options.background};`"
    id="relativeSvg"
  ></div>
</template>
<script setup lang="ts">
const props = defineProps<{
  data: {
    /**
     * 节点数据 {@link RelativeNode}
     */
    nodes: Array<RelativeNode>;
    /**
     * 连接线数据 {@link RelativeLink}
     */
    links: { source: string; label: string; target: string }[];
  };
  options: {
    /**
     * 画布背景
     */
    background: string;
    /**
     * 画布宽度
     */
    width: number;
    /**
     * 画布高度
     */
    height: number;
    /**
     * 节点边框颜色
     */
    nodeStroke: string;
    /**
     * 节点边框宽度
     */
    nodeStrokeWidth: number;
    /**
     * 节点半径
     */
    nodeRadius: number;
    /**
     * 节点文本字体大小
     */
    nodeLabelFontSize: number;
    /**
     * 节点背景类型
     * @propertities 1 纯色
     * @propertities 2 图片
     */
    nodeBackgroundType: "1" | "2";
    /**
     * 节点背景
     */
    nodeBackground: string;
    /**
     * 连接线颜色
     */
    linkStroke: string;
    /**
     * 连接线长度
     */
    linkLength: number;
    /**
     * 连接线宽度
     */
    linkStrokeWidth: number;
    /**
     * 连接线文本字体大小
     */
    linkLabelFontSize: number;
    /**
     * 连接线文本颜色
     */
    linkLabelColor: string;
    /**
     * 力模型
     * @propertities link 弹簧
     * @propertities collision 碰撞检测
     * @propertities positioning 重力
     * @propertities charge 电场
     * @propertities center 向心力
     * @propertities gravity 引力
     */
    forceModelList: Array<
      "link" | "collision" | "positioning" | "charge" | "center" | "gravity"
    >;
    dragable: boolean;
    nodeLabelColor: string;
    openBorder: boolean;
  };
}>();

onMounted(() => {
  appendSvg();
});
/**
 * 创建svg元素并加入文档
 */
function appendSvg() {
  const _links = props.data.links.map((link) => ({
    ...link,
    target: props.data.nodes.find((node) => node.id === link.target),
    source: props.data.nodes.find((node) => node.id === link.source),
  })) as RelativeLinkCompatible[];
  /**
   * 对向关系列表
   */
  const _reverseLinks: Array<Array<string>> = [];
  let _map: Record<string, string> = {};
  props.data.links.forEach((l) => (_map[`${l.source}-${l.target}`] = l.label));
  props.data.links.forEach((l) => {
    if (_map[`${l.target}-${l.source}`]) {
      delete _map[`${l.target}-${l.source}`];
      delete _map[`${l.source}-${l.target}`];
      _reverseLinks.push([l.source, l.target]);
    }
  });
  /**
   * 仿真器
   */
  const simulation: d3.Simulation<d3.SimulationNodeDatum, undefined> =
    d3.forceSimulation(props.data.nodes as d3.SimulationNodeDatum[]);

  // 添加弹簧模型
  if (props.options.forceModelList.indexOf("link") !== -1) {
    const forceLink = d3.forceLink(_links);
    forceLink.distance(props.options.linkLength);
    simulation.force("link", forceLink);
  }

  // 添加碰撞检测模型
  if (props.options.forceModelList.indexOf("collision") !== -1) {
    simulation.force("collision", d3.forceCollide(props.options.linkLength));
  }

  // 添加引力模型（强度值为正的电场）
  if (props.options.forceModelList.indexOf("gravity") !== -1) {
    const forceManyBody = d3.forceManyBody();
    simulation.force("charge", forceManyBody.strength(90));
  }

  // 添加电场模型
  if (props.options.forceModelList.indexOf("charge") !== -1) {
    const forceManyBody = d3.forceManyBody();
    simulation.force("charge", forceManyBody);
  }

  // 添加向心力模型
  if (props.options.forceModelList.indexOf("center") !== -1) {
    simulation.force("center", d3.forceCenter());
  }

  // 添加重力（指向下方的全局力）模型
  if (props.options.forceModelList.indexOf("positioning") !== -1) {
    const forceY = d3.forceY(props.options.height / 2);
    forceY.strength(0.78);
    simulation.force("positioning", forceY);
  }

  // 启动仿真
  simulation.on("tick", ticked).tick();

  const {
    /**
     * svg容器元素
     */
    SVG_CONTAINER,
    /**
     * 连线元素集合
     */
    LINK_ELEMENTS,
    /**
     * 节点元素集合
     */
    NODE_ELEMENTS,
    /**
     * 连线文本元素集合
     */
    LINK_LABEL,
    /**
     * 纯色背景节点文本元素集合
     */
    NODE_BG_COLOR_TEXT_ELEMENT,
  } = drawer();

  // 插入文档
  const SVG = document.getElementById("relativeSvg");
  SVG?.childNodes.forEach((node) => {
    SVG.removeChild(node);
  });
  SVG?.appendChild(SVG_CONTAINER.node() as Node);

  /**
   * 创建关系图元素
   */
  function drawer() {
    /**
     * svg容器元素
     */
    const SVG_CONTAINER = d3
      .create("svg")
      .attr("width", props.options.width)
      .attr("height", props.options.height)
      .attr("viewBox", [
        props.options.width / -2,
        props.options.height / -2,
        props.options.width,
        props.options.height,
      ])
      .attr("style", "max-width: 100%; height: auto; height: intrinsic;");

    /**
     * 连线容器元素
     */
    const LINK_CONTAINER = SVG_CONTAINER.append("g");

    /**
     * 连线元素集合
     */
    const LINK_ELEMENTS = LINK_CONTAINER.selectAll("line")
      .data(_links)
      .join("path")
      .attr("marker-end", "url(#arrow)")
      .attr("id", (l) => `linkID${l.source.id}-${l.target.id}`)
      .attr(
        "stroke",
        (l) =>
          (l as RelativeLinkCompatible)?.linkStroke || props.options.linkStroke
      )
      .attr("d", (l) =>
        getShortLinePosition(
          l.source as RelativeNodeCircle,
          l.target as RelativeNodeCircle,
          props.options.nodeRadius
        )
      );

    /**
     * 连线文本元素集合
     */
    const LINK_LABEL = LINK_CONTAINER.selectAll("text")
      .data(_links)
      .join("text")
      .attr(
        "font-size",
        (d) => d.linkLabelFontSize || props.options.linkLabelFontSize
      )
      .attr("text-anchor", "middle")
      .append("textPath")
      .attr("xlink:href", (l) => `#linkID${l.source.id}-${l.target.id}`)
      .attr("startOffset", "50%")
      .attr("fill", (d) => d.linkLabelColor || props.options.linkLabelColor)
      .text((l) => {
        return l.label;
      });

    /**
     * 箭头元素
     */
    const LINE_ARROW = SVG_CONTAINER.append("defs")
      .append("marker")
      .attr("id", "arrow")
      .attr("markerUnits", "strokeWidth")
      .attr("markerWidth", 24)
      .attr("markerHeight", 24)
      .attr("viewBox", "0 0 12 12")
      .attr("refX", 9)
      .attr("refY", 6)
      .attr("orient", "auto")
      .append("path")
      .attr("d", "M2,2 L10,6 L2,10 L6,6 L2,2")
      .attr("fill", props.options.linkStroke);

    /**
     * 节点容器
     */
    const NODE_CONTAINER = SVG_CONTAINER.append("g");

    /**
     * 节点元素集合
     */
    const NODE_ELEMENTS = NODE_CONTAINER.selectAll("circle")
      .data(props.data.nodes)
      .join("circle")
      .attr("stroke", (d) => d.nodeStroke || props.options.nodeStroke)
      .attr(
        "stroke-width",
        (d) => d.nodeStrokeWidth || props.options.nodeStrokeWidth
      )
      .attr("fill", function (d, i) {
        return `url(#circleImageID${d.id})`;
      })
      .attr("r", props.options.nodeRadius)
      .on("click", (e, d) => {
        // 节点点击事件
        console.log("点击事件", d, e);
        // alert('')
      });

    /**
     * 节点填充元素结合
     */
    const NODE_FILL_CONTAINER = NODE_CONTAINER.selectAll("defs")
      .data(props.data.nodes)
      .join("defs")
      .append("pattern")
      .attr("id", function (d, i) {
        return `circleImageID${d.id}`;
      })
      .attr("height", "1")
      .attr("width", "1")
      .attr("class", "circle-fill-pattern");

    // 底层背景填充
    NODE_FILL_CONTAINER.append("rect")
      .attr("stroke", (d) => d.nodeStroke || props.options.nodeStroke)
      .attr(
        "stroke-width",
        (d) => d.nodeStrokeWidth || props.options.nodeStrokeWidth
      )
      .attr("x", 0)
      .attr("y", 0)
      .attr("height", props.options.nodeRadius * 2)
      .attr("width", props.options.nodeRadius * 2)
      .attr("fill", (d) => {
        const { nodeBackground, nodeBackgroundType } = getNodeBackground(d);
        // 图片背景
        if (nodeBackgroundType === "2") {
          return "#FFFFFF";
        }
        return nodeBackground;
      });

    // 图片层背景填充
    NODE_FILL_CONTAINER.append("image")
      .attr("preserveAspectRatio", "none")
      .attr(
        "xlink:href",
        (d) => d.nodeBackground || props.options.nodeBackground
      )
      .attr("height", (d) => {
        const { nodeBackgroundType } = getNodeBackground(d);
        return nodeBackgroundType === "1" ? 0 : props.options.nodeRadius * 2;
      })
      .attr("width", (d) => {
        const { nodeBackgroundType } = getNodeBackground(d);
        return nodeBackgroundType === "1" ? 0 : props.options.nodeRadius * 2;
      });

    // 纯色背景，文本背景为节点内切正方形

    /**
     * 背景为图片时的填充元素
     */
    const NODE_FILL_CONTAINER_BG_IMAGE = NODE_FILL_CONTAINER.filter(
      (d) => getNodeBackground(d).nodeBackgroundType === "2"
    );
    NODE_FILL_CONTAINER_BG_IMAGE.append("rect")
      .attr("x", 0)
      .attr("y", 1.34 * props.options.nodeRadius)
      .attr("width", 2 * props.options.nodeRadius)
      .attr("height", 0.66 * props.options.nodeRadius)
      .attr("fill", "black")
      .attr("opacity", "0.3");
    NODE_FILL_CONTAINER_BG_IMAGE.append("text")
      .attr("x", function ({ name, x, nodeLabelFontSize }) {
        return (
          (2 * props.options.nodeRadius -
            name.length *
              (nodeLabelFontSize || props.options.nodeLabelFontSize)) *
          0.5
        );
      })
      .attr(
        "y",
        (d) =>
          1.34 * props.options.nodeRadius +
          (d.nodeLabelFontSize || props.options.nodeLabelFontSize) +
          2
      )
      .attr(
        "font-size",
        (d) => d.nodeLabelFontSize || props.options.nodeLabelFontSize
      )
      .attr("class", "circle-fill-label")
      .attr("stroke-width", 0)
      .attr("fill", props.options.nodeLabelColor)
      .text(function ({ name }) {
        return name;
      });

    if (props.options.dragable) {
      // 图片背景时，拖拽事件注册在节点元素上
      NODE_ELEMENTS.call(
        drag(simulation) as (
          selection: d3.Selection<
            d3.BaseType | SVGCircleElement,
            RelativeNode,
            SVGGElement,
            undefined
          >,
          ...args: any[]
        ) => void
      );
    }
    /**
     * 纯色背景节点文本元素集合
     */
    const NODE_BG_COLOR_TEXT_ELEMENT = NODE_CONTAINER.selectAll("foreignObject")
      .data(
        props.data.nodes.filter(
          (d) => getNodeBackground(d).nodeBackgroundType === "1"
        )
      )
      .join("foreignObject")
      // @ts-ignore
      .attr("x", (d) => d.x - props.options.nodeRadius * 0.707) // r * cos(45)
      // @ts-ignore
      .attr("y", (d) => d.y - props.options.nodeRadius * 0.707) // r * sin(45)
      .attr("width", props.options.nodeRadius * 1.414) // 2 * r * cos(45) = r * 根号2
      .attr("height", props.options.nodeRadius * 1.414) // 2 * r * sin(45) = r * 根号2

      .on("click", (e, d) => {
        // 节点点击事件
        console.log("点击事件", d, e);
        // alert('')
      });
    NODE_BG_COLOR_TEXT_ELEMENT.append("xhtml:div")
      .attr("title", (d) => d.name)
      .style("width", props.options.nodeRadius * 1.414 + "px")
      .style("height", props.options.nodeRadius * 1.414 + "px")
      .style("box-sizing", "border-box")
      .style("color", props.options.nodeLabelColor)
      .style(
        "font-size",
        (d) => (d.nodeLabelFontSize || props.options.nodeLabelFontSize) + "px"
      )
      .style("user-select", "none")
      .style("display", "flex")
      .style("align-items", "center")
      .style("justify-content", "center")
      .append("span")
      .text((d) => d.name);

    if (props.options.dragable) {
      // 纯色填充时，拖拽事件注册在节点内切正方形上
      NODE_BG_COLOR_TEXT_ELEMENT.call(
        drag(simulation) as (
          selection: d3.Selection<
            d3.BaseType | SVGCircleElement,
            RelativeNode,
            SVGGElement,
            undefined
          >,
          ...args: any[]
        ) => void
      );
    }
    return {
      SVG_CONTAINER,
      LINK_CONTAINER,
      NODE_CONTAINER,
      LINK_ELEMENTS,
      NODE_ELEMENTS,
      NODE_FILL_CONTAINER,
      LINK_LABEL,
      NODE_BG_COLOR_TEXT_ELEMENT,
    };
  }

  /**
   * 节点圆形
   */
  type RelativeNodeCircle = {
    x: number;
    y: number;
    nodeRadius: number;
    id: string;
  };

  /**
   * 仿真函数
   */
  function ticked() {
    // 封闭边界时进行边界检测
    if (!props.options.openBorder) {
      /**
       * 节点
       */
      const graphNodes = simulation.nodes();

      graphNodes.forEach((node: any) => {
        if (node.y + props.options.nodeRadius > props.options.height / 2) {
          node.y = props.options.height / 2 - props.options.nodeRadius;
          node.vy = 0;
        }
        if (node.y - props.options.nodeRadius < props.options.height / -2) {
          node.y = props.options.height / -2 + props.options.nodeRadius;
          node.vy = 0;
        }
        // @ts-ignore
        if (node.x + props.options.nodeRadius > props.options.width / 2) {
          node.x = props.options.width / 2 - props.options.nodeRadius;
          node.vx = 0;
        }
        if (node.x - props.options.nodeRadius < props.options.width / -2) {
          node.x = props.options.width / -2 + props.options.nodeRadius;
          node.vx = 0;
        }
      });
    }
    // 重算连线路径
    LINK_ELEMENTS.attr("d", (l) =>
      getShortLinePosition(
        l.source as RelativeNodeCircle,
        l.target as RelativeNodeCircle,
        props.options.nodeRadius
      )
    ).attr("marker-end", "url(#arrow)");

    // 重算节点路径
    NODE_ELEMENTS.attr(
      "cx",
      ((d: RelativeNode) => d.x) as TickValueFn<RelativeNode>
    ).attr("cy", ((d: RelativeNode) => d.y) as TickValueFn<RelativeNode>);
    // if (getNodeBackground() === "1") {
    NODE_BG_COLOR_TEXT_ELEMENT
      // @ts-ignore
      .attr("x", (d) => d.x - props.options.nodeRadius * 0.707) // r * cos(45)
      // @ts-ignore
      .attr("y", (d) => d.y - props.options.nodeRadius * 0.707); // r * sin(45)
    // }

    // 重算连线文本路径
    LINK_LABEL.attr(
      "x",
      ({ source, target, label, linkLabelFontSize }: RelativeLinkCompatible) =>
        // @ts-ignore
        (source?.x + target?.x) * 0.5 -
        (label.length *
          (linkLabelFontSize || props.options.linkLabelFontSize)) /
          2
      // @ts-ignore
    ).attr(
      "y",
      // @ts-ignore
      (d: RelativeLinkCompatible) => (d.source.y + d.target.y) / 2 + 7
    );
    // relations.attr('x', (d) => d.x - (d.relation.length * 16) / 2).attr('y', (d) => d.y + 16 / 2 + 20)
  }

  /**
   * 获取连线路径
   * @param source 起点圆
   * @param target 终点圆
   * @param r 默认半径
   */
  function getShortLinePosition(
    source: RelativeNodeCircle,
    target: RelativeNodeCircle,
    r: number
  ) {
    // 计算斜率
    let k = (source.y - target.y) / (source.x - target.x);
    // 反正切获取角度
    let angle = Math.abs(Math.atan(k));
    let [r1, r2] = [source.nodeRadius || r, target.nodeRadius || r];
    // 双线时计算偏移量
    let d = 0;
    if (
      _reverseLinks.findIndex(
        ([sid, tid]) =>
          (sid === source.id && tid === target.id) ||
          (sid === target.id && tid === source.id)
      ) !== -1
    ) {
      d = 1;
    }
    return `M ${
      // 端点坐标：余弦获取+x,正弦获取+y,存在反向箭头时旋转10°,k>0时端点逆时针,k<0时端点顺时针
      source.x +
      (source.x > target.x ? -1 : 1) *
        r1 *
        Math.cos(angle - ((k / Math.abs(k)) * (d * Math.PI)) / 18)
    },${
      source.y +
      (source.y > target.y ? -1 : 1) *
        r1 *
        Math.sin(angle - ((k / Math.abs(k)) * (d * Math.PI)) / 18)
    } L  ${
      // 终点坐标：余弦获取-x,正弦获取-y,存在反向箭头时旋转10°,k>0时端点顺时针,k<0时端点逆时针
      target.x +
      (source.x > target.x ? 1 : -1) *
        r2 *
        Math.cos(angle + ((k / Math.abs(k)) * (d * Math.PI)) / 18)
    },${
      target.y +
      (source.y > target.y ? 1 : -1) *
        r2 *
        Math.sin(angle + ((k / Math.abs(k)) * (d * Math.PI)) / 18)
    }`;
  }
  /**
   * 生成拖拽行为监听器函数
   * @param simulation 仿真器
   */
  function drag(simulation: d3.Simulation<d3.SimulationNodeDatum, undefined>) {
    function dragstarted(event: {
      active: any;
      subject: { fx: any; x: any; fy: any; y: any };
    }) {
      if (!event.active) simulation.alphaTarget(0.3).restart();
      event.subject.fx = event.subject.x;
      event.subject.fy = event.subject.y;
    }

    function dragged(event: { subject: { fx: any; fy: any }; x: any; y: any }) {
      event.subject.fx = event.x;
      event.subject.fy = event.y;
    }

    function dragended(event: {
      active: any;
      subject: { fx: null; fy: null };
    }) {
      if (!event.active) simulation.alphaTarget(0);
      event.subject.fx = null;
      event.subject.fy = null;
    }

    return d3
      .drag()
      .on("start", dragstarted)
      .on("drag", dragged)
      .on("end", dragended);
  }

  function getNodeBackground(d: RelativeNode): {
    nodeBackground: string;
    nodeBackgroundType: "1" | "2";
  } {
    if (d.nodeBackgroundType && d.nodeBackground) {
      return {
        nodeBackground: d.nodeBackground,
        nodeBackgroundType: d.nodeBackgroundType,
      };
    }
    return {
      nodeBackground: props.options.nodeBackground,
      nodeBackgroundType: props.options.nodeBackgroundType,
    };
  }
}

defineExpose({
  appendSvg,
});
</script>
<script lang="ts">
import * as d3 from "d3";
import { onMounted, defineExpose } from "vue";
import { RelativeNode, RelativeLink } from "./DataPanel.vue";
interface RelativeLinkCompatible extends d3.SimulationLinkDatum<RelativeNode> {
  source: RelativeNode;
  target: RelativeNode;
  label: string;
  /**
   * 连接线颜色
   */
  linkStroke?: string;
  /**
   * 连接线宽度
   */
  linkStrokeWidth?: number;
  /**
   * 连接线文本字体大小
   */
  linkLabelFontSize?: number;
  /**
   * 连接线文本颜色
   */
  linkLabelColor?: string;
}

/**
 * 节点-值仿真映射
 */
type TickValueFn<T> = d3.ValueFn<
  d3.BaseType | SVGLineElement,
  T,
  string | number | boolean | readonly (string | number)[] | null
>;
</script>
<style>
#relativeSvg {
  flex-grow: 1;
}
</style>
